package com.mspbots.sync.wise.service;

import cn.hutool.core.util.ReUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import com.mspbots.api.DataNotFoundException;
import com.mspbots.sync.BaseGenericService;
import com.mspbots.sync.model.ExceptionLog;
import com.mspbots.sync.model.TicketOnly;
import com.mspbots.sync.utils.WiseParamsUtils;
import com.mspbots.sync.utils.WiseResultBuildUtils;
import com.mspbots.sync.wise.client.ClientProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * com.mspbots.sync.wise.service.TicketEntryService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2019/10/18
 */
@Log4j2
@Service
@RequiredArgsConstructor
public class TicketEntryService extends BaseGenericService {

    private final CompanyService companyService;

    public void manualSyncTickets(ClientProperties configuration) {
        this.coreClient.get("ticket/rules/tenant/board",
                Map.of("tenantId", configuration.getTenantId(), "name", "STATUS_POINTS_GRADE_RULE"))
                .switchIfEmpty(Mono.error(new DataNotFoundException("This tenant " + configuration.getTenantId() + " status rules is empty.")))
                .map(ruleNode -> {
                    List<JsonNode> rules = this.objectMapper.convertValue(ruleNode, new TypeReference<>() {
                    });
                    List<Integer> status = rules.stream()
                            .map(rule -> this.objectMapper.convertValue(rule.get("params"), new TypeReference<List<JsonNode>>() {
                                    })
                            ).flatMap(params -> params.stream()
                                    .filter(item -> item.get("selected").asBoolean())
                                    .map(item -> item.get("status").asInt())
                            ).collect(Collectors.toList());
                    return Lists.partition(status, 30);
                })
                .flatMapMany(Flux::fromIterable)
                .map(StringUtils::collectionToCommaDelimitedString)
                .doOnNext(idsStr -> this.coreClient.delete("/tickets/delete-batch",
                        Map.of("tenantId", configuration.getTenantId(), "statusIds", idsStr), Integer.class).subscribe())
                .subscribeOn(Schedulers.parallel())
                .flatMap(idsStr -> this.cwWebClient.fetchUrl(UriComponentsBuilder.newInstance().scheme("https").host(configuration.getSite())
                        .path("/v4_6_release/apis/3.0/").path("/service/tickets")
                        .queryParams(WiseParamsUtils.paginationBuild("1",
                                "status/id in (" + idsStr + ")"))
                        .build().toUriString())
                        .mergeWith(this.cwWebClient.fetchUrl(UriComponentsBuilder.newInstance().scheme("https").host(configuration.getSite())
                                .path("/v4_6_release/apis/3.0/").path("/project/tickets")
                                .queryParams(WiseParamsUtils.paginationBuild("1",
                                        "status/id in (" + idsStr + ")"))
                                .build().toUriString())))
                .flatMap(jsonNode -> this.covertSetTicket(configuration, jsonNode))
                .subscribe(result -> {
                            log.info("This manual sync ticket id is {}", result.getCwId());
                            this.messagingTemplate.convertAndSend("msp.wise.callback.tickets",
                                    "", result);
                        },
                        err -> log.error("This tenant {} waitAndNext error.msg:{}", configuration.getTenantId(),
                                JSONUtil.toJsonStr(err)));
    }

    /**
     * this get tickets all sync first read redis time.
     * by this redis time [lastUpdated] > [lastSyncTime]
     *
     * @param queryParams this par ams default page=1,conditions={}
     * @return flux first page tickets.
     */
    public Flux<JsonNode> getEntriesOnly(Map<String, String> queryParams) {
        Integer tenant = Integer.parseInt(queryParams.get("tenantId"));
        queryParams.put("orderBy", "id desc");
        log.debug("This tenant {} get page tickets all params {}.", tenant, queryParams);
        return this.onGetWiseProperties(tenant)
                .flatMapMany(configuration ->
                        this.getEntries(configuration, WiseParamsUtils.mapToMultiValueMapBuild(queryParams)));

    }


    /**
     * this get tickets all sync first read redis time.
     * by this redis time [lastUpdated] > [lastSyncTime]
     *
     * @param configuration this tenant connect wise config
     * @return flux one ticket only.
     */
    public Flux<TicketOnly> getEntries(ClientProperties configuration) {
        return this.loadRedisValue("sync-ticket-entries-" + configuration.getTenantId())
                .doOnNext(lastTime -> log.debug("Start {} sync tickets entry by redis lastSyncTime {}.",
                        configuration.getTenantId(), lastTime))
                .map(lastSyncTime -> "lastUpdated > [" + lastSyncTime + "]")
                .map(queryStr -> WiseParamsUtils.paginationBuild("1", queryStr))
                .flatMapMany(params -> this.getEntries(configuration, params))
                .switchIfEmpty(Mono.error(new DataNotFoundException("This tenant "
                        + configuration.getTenantId() + " get ticket entries is empty.")))
                .flatMap(jsonNode -> this.covertSetTicket(configuration, jsonNode))
                .flatMap(ticketOnly -> this.covertSetTicketAudit(configuration, ticketOnly))
                .doOnNext(entry -> this.loadRedisValue("sync-ticket-entries-" + configuration.getTenantId())
                        .subscribe(syncTime -> {
                            if (syncTime.isBefore(entry.getUpdatedTime())) {
                                this.recordRedisValue("sync-ticket-entries-" + configuration.getTenantId(),
                                        entry.getUpdatedTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
                            }
                        }));
    }

    public Mono<TicketOnly> covertSetTicket(ClientProperties configuration, JsonNode jsonNode) {
        return Mono.just(WiseResultBuildUtils.buildTicketOnly(configuration.getTenantId(), jsonNode))
                .flatMap(ticketOnly -> this.setCompanyTeamRoles(configuration, ticketOnly))
                .flatMap(ticketOnly -> this.setConfigurations(configuration, ticketOnly));
    }

    private Mono<TicketOnly> covertSetTicketAudit(ClientProperties configuration, TicketOnly ticketOnly) {
        return this.setStatusLastTime(configuration, ticketOnly);
    }

    public Mono<JsonNode> getAuditTrail(Map<String, String> queryParams) {
        Integer tenant = Integer.parseInt(queryParams.get("tenantId"));
        return this.onGetWiseProperties(tenant)
                .flatMap(configuration -> {
                    MultiValueMap<String, String> params = WiseParamsUtils.paginationBuild("1", null);
                    params.set("id", queryParams.get("id"));
                    return this.getAuditTrail(configuration, params).collectList()
                            .map(list -> this.objectMapper.convertValue(list, JsonNode.class));
                });

    }

    public Flux<JsonNode> getNotes(Map<String, String> queryParams) {
        Integer tenant = Integer.parseInt(queryParams.get("tenantId"));
        long id = Long.parseLong(queryParams.get("id"));

        return this.onGetWiseProperties(tenant)
                .flatMapMany(configuration -> super.cwWebClient.getRepository("service/tickets/" + id + "/notes",
                        WiseParamsUtils.queryParamsBuild("1", null))
                        .subscriberContext(ctx -> ctx.put("properties", configuration)));

    }

    public Mono<Long> getEntryCount(Map<String, String> queryParams) {
        Integer tenant = Integer.parseInt(queryParams.get("tenantId"));
        return this.onGetWiseProperties(tenant)
                .flatMapMany(configuration -> cwWebClient.getRepository("/service/tickets/count",
                        WiseParamsUtils.mapToMultiValueMapBuild(queryParams))
                        .map(jsonNode -> jsonNode.get("count").asLong())
                        .mergeWith(cwWebClient.getRepository("/project/tickets/count",
                                WiseParamsUtils.mapToMultiValueMapBuild(queryParams))
                                .map(jsonNode -> jsonNode.get("count").asLong()))
                        .subscriberContext(ctx -> ctx.put("properties", configuration)))
                .collectList()
                .map(list -> list.stream().mapToLong(s -> s).sum())
                .defaultIfEmpty(0L);
    }

    private Mono<TicketOnly> setCompanyTeamRoles(ClientProperties configuration, TicketOnly ticketOnly) {
        log.debug("Ticket team roles by id{}", ticketOnly.getCwId());
        return this.companyService.getCompanyTeamRoles(configuration, ticketOnly.getCompanyId())
                .collectList()
                .map(jsonNodeList -> {
                    if (jsonNodeList.size() > 0) {
                        ticketOnly.setTeamRoles(this.objectMapper.convertValue(jsonNodeList, ArrayNode.class));
                    }
                    return ticketOnly;
                }).defaultIfEmpty(ticketOnly);
    }

    private Mono<TicketOnly> setStatusLastTime(ClientProperties configuration, TicketOnly ticketOnly) {
        log.debug("Ticket status last time  by id{}", ticketOnly.getCwId());
        return this.getStatusLastTime(configuration, ticketOnly.getCwId(), ticketOnly.getStatus())
                .defaultIfEmpty(ticketOnly.getUpdatedTime())
                .map(statusTime -> {
                    ObjectNode audiNode = this.objectMapper.createObjectNode();
                    audiNode.put("id", ticketOnly.getStatusId());
                    audiNode.put("name", ticketOnly.getStatus());
                    audiNode.put("time", statusTime.format(DateTimeFormatter.ISO_DATE_TIME));
                    ticketOnly.setAuditor(audiNode);
                    return ticketOnly;
                });
    }

    private Mono<TicketOnly> setConfigurations(ClientProperties configuration, TicketOnly ticketOnly) {
        JsonNode infoNode = ticketOnly.getExtend().get("_info");
        log.debug("_info json result str {}", infoNode);
        if (!ObjectUtils.isEmpty(infoNode) && !ObjectUtils.isEmpty(infoNode.get("configurations_href"))) {
            return this.cwWebClient.fetchUrl(infoNode.get("configurations_href").asText())
                    .subscriberContext(ctx -> ctx.put("properties", configuration))
                    .collectList()
                    .map(jsonNodeList -> {
                        if (jsonNodeList.size() > 0) {
                            ticketOnly.setConfigurations(this.objectMapper.convertValue(jsonNodeList, ArrayNode.class));
                        }
                        return ticketOnly;
                    }).defaultIfEmpty(ticketOnly);
        }
        return Mono.just(ticketOnly);
    }

    private Flux<JsonNode> getEntries(ClientProperties configuration, MultiValueMap<String, String> params) {
        log.debug("This tenant {} get no page tickets params {}.", configuration.getTenantId(), params);
        return this.cwWebClient.fetch("/service/tickets", params)
                .switchMap(jsonNode -> {
                    if (ObjectUtils.isEmpty(jsonNode.get("id"))) {
                        log.error("This tenant sync ticket error.msg: {}", jsonNode);
                        this.messagingTemplate.convertAndSend("exception.log", ExceptionLog.builder()
                                .tenantId(configuration.getTenantId()).name("Connect wise")
                                .status(500).type("SYNC-WISE-TICKET").msg("Connect wise sync ticket entry error.")
                                .extend(jsonNode).build());
                        return Flux.empty();
                    }
                    return Flux.just(jsonNode);
                })
                .mergeWith(this.cwWebClient.fetch("/project/tickets", params)
                        .switchMap(jsonNode -> {
                            if (ObjectUtils.isEmpty(jsonNode.get("id"))) {
                                log.error("This tenant sync ticket error.msg: {}", jsonNode);
                                this.messagingTemplate.convertAndSend("exception.log", ExceptionLog.builder()
                                        .tenantId(configuration.getTenantId()).name("Connect wise")
                                        .status(500).type("SYNC-WISE-TICKET").msg("Connect wise sync ticket entry error.")
                                        .extend(jsonNode).build());
                                return Flux.empty();
                            }
                            return Flux.just(jsonNode);
                        }))
                .subscriberContext(ctx -> ctx.put("properties", configuration));
    }

    private Mono<LocalDateTime> getStatusLastTime(ClientProperties configuration, long ticketId, String statusName) {
        String regex = "(?<=\\bto\\b\\s?\")(?<=\").*?(?=\")";
        MultiValueMap<String, String> params = WiseParamsUtils.paginationBuild("1", null);
        params.set("id", ticketId + "");
        return this.getAuditTrail(configuration, params)
                .filter(jsonNode -> !ObjectUtils.isEmpty(jsonNode.get("auditSubType")))
                .filter(jsonNode -> "Status".equals(jsonNode.get("auditSubType").asText())
                        && ReUtil.get(regex, jsonNode.get("text").asText(), 0).equals(statusName))
                .map(jsonNode -> LocalDateTime.parse(jsonNode.findValue("enteredDate").asText(), DateTimeFormatter.ISO_DATE_TIME))
                .last();
    }


    private Flux<JsonNode> getAuditTrail(ClientProperties configuration, MultiValueMap<String, String> params) {
        log.debug("Ticket get audit trail params :{}", params);
        Assert.notEmpty(params.get("id"), () -> "query ticket params id is required.");
        params.set("type", "Ticket");
        return this.cwWebClient.fetch("/system/audittrail", params)
                .subscriberContext(ctx -> ctx.put("properties", configuration));
    }


}
